﻿using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Options;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using Newtonsoft.Json.Linq;
using ProductCatalog.WebApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MongoDB.Driver.Linq;
using System.Text;

namespace ProductCatalog.WebApi.DataLayer
{
    public class ProductRepository
    {
        MongoCollection<BsonDocument> products;
        public ProductRepository()
        {
            var client = new MongoClient(CloudConfiguration.GetMongolabConnectionString());
            var server = client.GetServer();
            var database = server.GetDatabase(CloudConfiguration.GetProductDatabaseName());
            products = database.GetCollection<BsonDocument>(CloudConfiguration.GetProductCollectionName());
        }

        public Product GetProduct(string id)
        {
            ObjectId objId;
            if (ObjectId.TryParse(id, out objId))
            {
                var doc = products.FindOneById(objId);
                return doc.ToProduct();
            }
            else
                return null;
        }

        public string AddProductDetails(JObject json)
        {
            var doc = BsonDocument.Parse(json.ToString());
            List<string> missingProperties;
            if (!doc.HasRequiredProperties(out missingProperties))
            {
                // Try to extract required properties from data.
                if (!doc.TryAddMissingProperties(missingProperties))
                {
                    StringBuilder sbMissingProps = new StringBuilder();
                    foreach (string s in missingProperties)
                        sbMissingProps.Append("'" + s + "' ");
                    throw new MissingFieldException(
                        string.Format("Data is missing the following required fields: {0}", sbMissingProps.ToString()));
                }
            }

            // Insert the document into products DB.
            var result = products.Insert(doc);
            if (result.Ok)
                return doc["_id"].ToString();
            else
                return "";
        }

        public JObject GetProductDetails(string id)
        {
            // Niko 
            ObjectId objId;
            if (ObjectId.TryParse(id, out objId))
            {
                var doc = products.FindOneById(objId);

                #region retrieve average review for productid

                var countQuery = @"
{
    $match:{
        _id: ObjectId('XXX')
    }
},
{
    $project:{
        reviews:1
    }
},
{
    $unwind:'$reviews'
},
{
    $group:{
        _id: 'result',
        reviewAvg: { $avg: '$reviews.rating' }
    }
}";
                // hate me for this .. but this way the json stays clean and without double {{, which would be neccessary for string format
                countQuery = countQuery.Replace("XXX", id);

                var result = products.Aggregate(MongoHelper.ParseToBsonDocumentArray(countQuery));
                var resultDoc = result.ResultDocuments
                    .SingleOrDefault();

                double avgRating = 0;
                if (resultDoc != null)
                    avgRating = Math.Round(resultDoc["reviewAvg"].ToDouble(), 2);

                #endregion

                return doc.ToProductDetails(avgRating);
            }
            else
                return null;
        }

        public Reviews GetProductReviews(string prodid, int skip, int limit)
        {
            // Niko

            #region retrieve reviews for productid

            ObjectId objId;
            if (!ObjectId.TryParse(prodid, out objId))
                return null;

            var reviewQuery = @"
{
    $match:{
        _id: ObjectId('XXXIDXXX')
    }
},
{
    $project:{
        _id: 0,
        reviews: 1
    }
},
{
    $unwind:'$reviews'
},
{
    $skip: XXXSKIPXXX
},
{
    $limit: XXXLIMITXXX
}";
            // hate me for this .. but this way the json stays clean and without double {{, which would be neccessary for string format
            reviewQuery = reviewQuery.Replace("XXXIDXXX", prodid);
            reviewQuery = reviewQuery.Replace("XXXSKIPXXX", skip.ToString());
            reviewQuery = reviewQuery.Replace("XXXLIMITXXX", limit.ToString());

            var reviews = products.Aggregate(MongoHelper.ParseToBsonDocumentArray(reviewQuery))
                .ResultDocuments
                .Select(i => i["reviews"].AsBsonDocument.ToReview())
                .ToArray();

            #endregion

            #region retrieve review count for productid

            var countQuery = @"
{
    $match:{
        _id: ObjectId('XXX')
    }
},
{
    $project:{
        reviews:1
    }
},
{
    $unwind:'$reviews'
},
{
    $group:{
        _id: 'result',
        reviewCount: { $sum: 1 }
    }
}";
            // hate me for this .. but this way the json stays clean and without double {{, which would be neccessary for string format
            countQuery = countQuery.Replace("XXX", prodid);

            var result = products.Aggregate(MongoHelper.ParseToBsonDocumentArray(countQuery));
            var resultDoc = result.ResultDocuments
                .SingleOrDefault();
            int count = 0;
            if (resultDoc != null) 
                count = resultDoc["reviewCount"].ToInt32();

            #endregion

            return new Reviews()
            {
                totalCount = count,
                reviews = reviews
            };
        }

        public string AddProductReview(string prodId, Review review)
        {
            //Rachel
            var prod = products.FindOneById(ObjectId.Parse(prodId));

            // Generate an ID for the review.  This needs to be returned to the client in
            // the location header of the response.
            review.reviewId = ObjectId.GenerateNewId().ToString();
            review.prodId = prodId;
            var revobj = new BsonDocument().
                                Add("reviewId", review.reviewId).
                                Add("prodId", review.prodId).
                                Add("userId", review.userId).
                                Add("comments", review.comments).
                                Add("rating", review.rating).
                                Add("reviewDate", review.reviewDate);

            if (prod.Contains("reviews"))
            {
                prod["reviews"].AsBsonArray.Add(BsonValue.Create(revobj));
            }
            else
            {
                prod["reviews"] = new BsonArray().Add(BsonValue.Create(revobj));
            }

            products.Save(prod);
            return review.reviewId;
        }

        public Review GetProductReview(string prodid, string reviewid)
        {
            // Find Product 
            ObjectId objId;
            if (!ObjectId.TryParse(prodid, out objId))
                return null;

            var prod = products.FindOneById(objId);

            if (prod == null) return null;
            // Scan for reviews
            if (!prod.Contains("reviews")) return null; // No reviews


            foreach (BsonValue r in prod["reviews"].AsBsonArray)
            {
                var reviewDoc = r.AsBsonDocument;
                if (reviewDoc["reviewId"] == reviewid)
                {
                    // Set the containing product Id for the "review" document.
                    reviewDoc["prodId"] = prodid;
                    return reviewDoc.ToReview();
                }
            }

            return null;
        }

        public bool UpdateProductReview(Review review)
        {
            bool bUpdated = false;

            // Find Product 
            ObjectId objId;
            if (ObjectId.TryParse(review.prodId, out objId))
            {
                var prod = products.FindOneById(objId);
                foreach (BsonValue r in prod["reviews"].AsBsonArray)
                {
                    var reviewDoc = r.AsBsonDocument;
                    if (reviewDoc["reviewId"] == review.reviewId)
                    {
                        // Update the review with new review data.
                        reviewDoc["reviewDate"] = review.reviewDate;
                        reviewDoc["userId"] = review.userId;
                        reviewDoc["rating"] = review.rating;
                        reviewDoc["comments"] = review.comments;

                        // Save the containing product document and get
                        // out of this loop - we're done!
                        products.Save(prod);
                        bUpdated = true;
                        break;
                    }
                }

            }
            return bUpdated;
        }

        public void DeleteProductsAndReviews()
        {
            products.Drop();
        }

        public ProductSearchResult GetProductListByIds(string[] prodIds)
        {
            // Niko
            var idValues = new BsonArray();
            foreach (string id in prodIds)
                idValues.Add(new ObjectId(id));

            var query = Query.In("_id", idValues);

            var producstByIds = products.Find(query)
                .Select(d => d.ToProduct());

            #region retrieve review count for productid

            var countQuery = @"
{
    $match:{
        _id: { $in: [XXX] }
    }
},
{
    $group:{
        _id: 'result',
        productCount: { $sum: 1 }
    }
}";
            // creates a string like this:
            // ObjectId('5178f823bbb2732280d139cd'),ObjectId('517aaa380fdb8c0b2cfda211')
            var objectIdArrayString = string.Join(",", prodIds.Select(id => string.Format("ObjectId('{0}')", id)));

            // hate me for this .. but this way the json stays clean and without double {{, which would be neccessary for string format
            countQuery = countQuery.Replace("XXX", objectIdArrayString);

            var result = products.Aggregate(MongoHelper.ParseToBsonDocumentArray(countQuery));
            var resultDoc = result.ResultDocuments
                .SingleOrDefault();
            var count = resultDoc["productCount"]
                .ToInt32();

            #endregion

            return new ProductSearchResult()
            {
                totalCount = count,
                Products = producstByIds
            };
        }

        public ProductSearchResult GetProductListByName(string srchstr)
        {
            // Making an assumption here that "prodName" will be in the document.
            var q = Query.Matches("prodName", new BsonRegularExpression(srchstr, "i"));
            var docs = products.Find(q).ToList();
            var prodList = new List<Product>();
            foreach (BsonDocument d in docs)
                prodList.Add(d.ToProduct());

            ProductSearchResult srchResult = new ProductSearchResult()
            {
                totalCount = docs.Count,
                Products = prodList
            };

            return srchResult;
        }

        public ProductSearchResult GetProductListByTag(string srchstr)
        {
            // Niko
            var query = Query.EQ("tags", srchstr);

            var producstByTag = products.Find(query)
                .Select(d => d.ToProduct());

            #region retrieve review count for productid

            var countQuery = @"
{
    $match:{
        tags: 'XXX'
    }
},
{
    $group:{
        _id: 'result',
        productCount: { $sum: 1 }
    }
}";
            // hate me for this .. but this way the json stays clean and without double {{, which would be neccessary for string format
            countQuery = countQuery.Replace("XXX", srchstr);

            var result = products.Aggregate(MongoHelper.ParseToBsonDocumentArray(countQuery));
            var resultDoc = result.ResultDocuments
                .SingleOrDefault();
            var count = resultDoc["productCount"]
                .ToInt32();

            #endregion

            return new ProductSearchResult()
            {
                totalCount = count,
                Products = producstByTag
            };
        }

        public IEnumerable<string> GetAvailableTags()
        {
            return products.Distinct<string>("tags", null);
        }
    }
}